﻿using System;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Collections.ObjectModel;

using LeanEngine.Entity;
using LeanEngine.Extensions;
using LeanEngine.Data.Linq.Language;
using LeanEngine.Data.Linq.Mapping;
using LeanEngine.Data.Linq.Extension;

namespace LeanEngine.Data.Linq.Translator
{
    internal sealed class JoinTranslator : LinqTranslator<List<JoinToken>>
    {
        #region Declaration Members

        private List<JoinToken> _joinList;
        private JoinToken _currentToken;
        private IQueryToken _currentCompare;
        private QueryContext _context;

        #endregion

        #region Constructor Members

        public JoinTranslator()
        {
            _joinList = new List<JoinToken>();
        }

        #endregion

        #region PropertyInfo Members

        internal ReadOnlyCollection<MemberBinding> BindingsList { get; private set; }

        #endregion

        #region LinqTranslator<List<JoinToken>> Members

        internal override bool CanEvaluate(QueryContext context)
        {
            switch (context.CurrentMethodCall.Method.Name)
            {
                case "Join": return true;
                default: return false;
            }
        }

        internal override List<JoinToken> Interpret(QueryContext context)
        {
            _context = context;

            this.EvaluateMethodCall(context.CurrentMethodCall);

            return _joinList;
        }

        #endregion

        #region Overrided Method Members

        protected override Expression EvaluateMethodCall(MethodCallExpression m)
        {
            MemberExpression arg = null;

            switch (m.Method.Name)
            {
                case "AsJoinRelation":
                    arg = m.Arguments[0] as MemberExpression;
                    TranslateMultiPartJoinRelation(arg.Expression.Type, m.Method.ReturnType, JoinType.INNER, arg);
                    TranslateMultiPartFieldRelation(m.Method.ReturnType, arg);
                    break;

                case "AsLeftJoinRelation":
                    arg = m.Arguments[0] as MemberExpression;
                    TranslateMultiPartJoinRelation(arg.Expression.Type, m.Method.ReturnType, JoinType.LEFT, arg);
                    TranslateMultiPartFieldRelation(m.Method.ReturnType, arg);
                    break;

                case "AsRightJoinRelation":
                    arg = m.Arguments[0] as MemberExpression;
                    TranslateMultiPartJoinRelation(arg.Expression.Type, m.Method.ReturnType, JoinType.RIGHT, arg);
                    TranslateMultiPartFieldRelation(m.Method.ReturnType, arg);
                    break;

                default:
                    _currentToken = new JoinToken();
                    var innerMethod = m.Arguments[1] as MethodCallExpression;

                    if (innerMethod != null && innerMethod.Method.Name == "AsLeftJoin") _currentToken.JoinType = JoinType.LEFT;
                    else if (innerMethod != null && innerMethod.Method.Name == "AsRightJoin") _currentToken.JoinType = JoinType.RIGHT;
                    
                    _currentCompare = new CompareToken() { Operator = CompareOperators.Equals };
                    _currentToken.JoinedTable = GetExpressionValue(m.Arguments[1]);
                    
                    base.Evaluate(m.Arguments[2]);
                    base.Evaluate(m.Arguments[3]);
                    if (m.Arguments.Count > 3) TranslateMemberInit(m.Arguments[4]);
                    
                    _currentToken.PredicateList.Add(_currentCompare);
                    var index = _joinList.Count - 2 < 0 ? 0 : _joinList.Count - 2;
                    _joinList.Insert(index, _currentToken);
                    break;
            }

            return m;
        }

        protected override Expression EvaluateLambda(LambdaExpression lambda)
        {
            var compareToken = _currentCompare as CompareToken;
            if (compareToken != null && compareToken.Field != null && compareToken.CompareValue.IsNullOrEmpty() && lambda.Body.NodeType == ExpressionType.Parameter)
            {
                TranslateEntityRelation(lambda.Body.Type);
                var fieldCompare = _currentCompare as FieldCompareToken;
                if (fieldCompare != null)
                {
                    fieldCompare.ComparedFieldName = compareToken.Field.Name;
                }
                return lambda;
            }

            return base.EvaluateLambda(lambda);
        }

        protected override Expression EvaluateMemberAccess(MemberExpression m)
        {
            MemberExpression innerMember = m;
            while (innerMember.Expression.NodeType == ExpressionType.MemberAccess) innerMember = innerMember.Expression as MemberExpression;

            var tableType = innerMember.Expression.Type.Name.Contains("AnonymousType")
                ? innerMember.Type
                : innerMember.Expression.Type;

            var memberName = innerMember.Expression.Type.Name.Contains("AnonymousType")
                ? (m.Expression as MemberExpression).Member.Name
                : innerMember.Member.Name;

            var table = _context.QueryableTableList.Where(tableType);
            var column = table.Columns.Where(c => c.PropertyName == memberName).FirstOrDefault();
            if (column == null) column = table.Columns.Where(c => c.PropertyName == m.Member.Name).First();
            var compareToken = _currentCompare as CompareToken;
            if (compareToken.Field == null)
            {
                compareToken.Field = column;
            }
            else
            {
                compareToken.CompareValue = column.GetStatement();
            }

            return m;
        }

        protected override Expression EvaluateParameter(ParameterExpression p)
        {
            if (p.NodeType != ExpressionType.Parameter)
            {
                TranslateEntityRelation(p.Type);
            }

            return base.EvaluateParameter(p);
        }

        #endregion

        #region Method Members

        private void TranslateMemberInit(Expression exp)
        {
            var unary = exp as UnaryExpression;
            if (unary != null)
            {
                var lambda = unary.Operand as LambdaExpression;
                if (lambda != null)
                {
                    var memberInit = lambda.Body as MemberInitExpression;
                    if(memberInit != null) this.BindingsList = memberInit.Bindings;
                }
            }
        }

        private void TranslateEntityRelation(Type tableType)
        {
            var compareToken = _currentCompare as CompareToken;
            var table = _context.QueryableTableList.Where(tableType);
            var isFirst = true;

            foreach (var column in table.Metadata.ColumnsList.WherePrimaryKey())
            {
                if (isFirst)
                {
                    _currentCompare = new FieldCompareToken()
                    {
                        Operator = compareToken.Operator,
                        CombineOperator = compareToken.CombineOperator,
                        ComparedTable = compareToken.Field.Table,
                        ComparedFieldName = column.DBColumn.Name,
                        FieldName = column.DBColumn.Name,
                        Table = table
                    };
                    isFirst = false;
                }
                else
                {
                    _currentToken.PredicateList.Add(new FieldCompareToken()
                    {
                        Operator = CompareOperators.Equals,
                        CombineOperator = CompareOperators.AndAlso,
                        ComparedTable = compareToken.Field.Table,
                        ComparedFieldName = column.DBColumn.Name,
                        FieldName = column.DBColumn.Name,
                        Table = table
                    });
                }
            }
        }

        private void TranslateMultiPartFieldRelation(Type rightTableType, MemberExpression exp)
        {
            var compareToken = _currentCompare as CompareToken;
            var multiPartJoin = _currentToken as MultiPartJoinToken;
            var rightTable = _context.QueryableTableList.Where(rightTableType);
            var isFirst = true;

            foreach (var pkColumn in rightTable.Metadata.ColumnsList.WherePrimaryKey())
            {
                var field = multiPartJoin.AssociationTable.Columns.Where(c => c.Name == pkColumn.DBColumn.Name).First();
                var compareColumn = rightTable.Columns.Where(c => c.Name == pkColumn.DBColumn.Name).First();

                if (isFirst)
                {
                    compareToken.Field = field;
                    compareToken.CompareValue = compareColumn.GetStatement();
                    isFirst = false;
                }
                else
                {
                    _currentToken.PredicateList.Add(new CompareToken()
                    {
                        Operator = CompareOperators.Equals,
                        CombineOperator = CompareOperators.AndAlso,                        
                        CompareValue = compareColumn.GetStatement(),
                        Field = field
                    });
                }
            }
        }

        private void TranslateMultiPartJoinRelation(Type leftTableType, Type rightTableType, JoinType joinType, MemberExpression exp)
        {
            var property = exp.Member as PropertyInfo;
            var associationAttribute = property.GetCustomAttributes(typeof(AssociationTableAttribute), false).First() as AssociationTableAttribute;
            var leftTable = _context.QueryableTableList.Where(leftTableType);
            var rightTable = _context.QueryableTableList.Where(rightTableType);
            var associationTable = new TableToken(associationAttribute.Name, leftTable, rightTable);
            var predicateList = new List<IQueryToken>();

            foreach (var metaColumn in leftTable.Metadata.ColumnsList.WherePrimaryKey())
            {
                var column = leftTable.Columns.Where(c => c.Name == metaColumn.DBColumn.Name).First();
                var predicate = new CompareToken()
                    {
                        Field = associationTable.Columns.Where(c => c.Name == metaColumn.DBColumn.Name).First(),
                        Operator = CompareOperators.Equals,
                        CompareValue = column.GetStatement(),
                        CombineOperator = CompareOperators.AndAlso
                    };

                predicateList.Add(predicate);
            }

            (predicateList.Last() as CompareToken).CombineOperator = string.Empty;

            _currentToken = new MultiPartJoinToken()
            {
                AssociationJoinType = joinType,
                AssociationTable = associationTable,
                AssociationPredicateList = predicateList,
                JoinedTable = _currentToken.JoinedTable,
                JoinType = _currentToken.JoinType,
                PredicateList = _currentToken.PredicateList
            };
        }

        private TableToken GetExpressionValue(Expression expression)
        {
            var methodCall = expression as MethodCallExpression;
            var constant = expression as ConstantExpression;

            if (methodCall != null) constant = methodCall.Arguments[0] as ConstantExpression;

            var query = constant.Value as IQueryable;
            var value = query == null ? constant.Value as IQueryableTable : ((ConstantExpression)query.Expression).Value as IQueryableTable;

            return _context.QueryableTableList.Where(value.MetaTable.EntityType);
        }

        #endregion
    }
}
